﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Globalization;
using System.Linq;
using System.Reflection;

namespace BrandonHaynes.Data
	{
	public class ReflectedDataRecordAdapter : IDataRecord
		{
		private static IDictionary<Type, MetadataCollection> PropertyCache = new Dictionary<Type, MetadataCollection>();
		private static IList<string> methodFilter = new List<string>() { "ToString", "Dispose", "GetType" };

		private object Adaptee { get; set; }
		private MetadataCollection Properties { get; set; }

		#region Static Methods

		private static MetadataCollection GenerateProperties(object adaptee)
			{
			return new MetadataCollection(adaptee.GetType()
				// Properties
				.GetProperties()
				.Where(property => !property.GetIndexParameters().Any())
								.Select(property => new Metadata(property.Name, BindingFlags.GetProperty))
				
				// Methods
				.Union(adaptee.GetType().GetMethods()
					.Where(method => !method.GetParameters().Any() && (method.Attributes & MethodAttributes.SpecialName) != MethodAttributes.SpecialName)
					.Where(method => !methodFilter.Contains(method.Name))
					.Select(method => new Metadata(FormatName(method.Name), method.Name, BindingFlags.InvokeMethod)))

				// Self
				.Union(Enumerable.Repeat(new Metadata("Value", string.Empty, BindingFlags.Default), 1)));
			}

		private static string FormatName(string name)
			{
			return name.StartsWith("Get", StringComparison.OrdinalIgnoreCase) ?
				name.Substring(3) :
				name;
			}
		#endregion

		#region Constructors
		public ReflectedDataRecordAdapter(object adaptee)
			{
			if (!PropertyCache.ContainsKey(adaptee.GetType()))
				PropertyCache.Add(adaptee.GetType(), GenerateProperties(adaptee));

			Properties = PropertyCache[adaptee.GetType()];
			Adaptee = adaptee;
			}

		//public DataRecordAdapter(object adaptee, IDictionary<string, string> hints)
		//    {
		//    throw new NotImplementedException();
		//    }
		#endregion

		#region Private Methods
		private object this[Metadata value]
			{
			get
				{
				return string.IsNullOrEmpty(value.ReflectedName) ? 
					Adaptee :
					Adaptee.GetType().InvokeMember(value.ReflectedName, value.Binding, null, Adaptee, null, CultureInfo.InvariantCulture)
					?? DBNull.Value;
				}
			}
		#endregion

		#region IDataRecord Members

		public int FieldCount
			{ get { return Properties.Count; } }

		public bool GetBoolean(int i)
			{ return (bool)this[i]; }

		public byte GetByte(int i)
			{ return (byte)this[i]; }

		public long GetBytes(int i, long fieldOffset, byte[] buffer, int bufferoffset, int length)
			{
			var data = (byte[])this[i];

			if(length > data.Length)
				length = data.Length;

			Array.Copy(data, fieldOffset, buffer, bufferoffset, length);

			return length;
			}

		public char GetChar(int i)
			{ return (char)this[i]; }

		public long GetChars(int i, long fieldoffset, char[] buffer, int bufferoffset, int length)
			{
			var data = (char[])this[i];

			if (length > data.Length)
				length = data.Length;

			Array.Copy(data, fieldoffset, buffer, bufferoffset, length);

			return length;
			}

		public IDataReader GetData(int i)
			{
			return new ReflectedDataReaderAdapter(Enumerable.Repeat(this[i], 1));
			}

		public string GetDataTypeName(int i)
			{ return GetFieldType(i).Name; }

		public DateTime GetDateTime(int i)
			{ return (DateTime)this[i]; }

		public decimal GetDecimal(int i)
			{ return (decimal)this[i]; }

		public double GetDouble(int i)
			{ return (double)this[i]; }

		public Type GetFieldType(int i)
			{ return this[i].GetType(); }

		public float GetFloat(int i)
			{ return (float)this[i]; }

		public Guid GetGuid(int i)
			{ return (Guid)this[i]; }

		public short GetInt16(int i)
			{ return (short)this[i]; }

		public int GetInt32(int i)
			{ return (int)this[i]; }

		public long GetInt64(int i)
			{ return (long)this[i]; }

		public string GetName(int i)
			{ return Properties[i].Name; }

		public int GetOrdinal(string name)
			{ return Properties.IndexOf(name); }

		public string GetString(int i)
			{ return (string)this[i]; }

		public object GetValue(int i)
			{ return this[i]; }

		public int GetValues(object[] values)
			{
			for (int index = 0; index < this.FieldCount; index++)
				values[index] = this[index];

			return this.FieldCount;
			}

		public bool IsDBNull(int i)
			{ return this[i] == null; }

		public object this[string name]
			{
			get { return this[Properties[name]]; }
			}

		public object this[int i]
			{
			get { return this[Properties[i]]; }
			}

		#endregion
		}
	}
